/**
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Nicola Lagomarsini, GeoSolutions S.A.S., Copyright 2014
*
*/
package org.geowebcache.io;
import it.geosolutions.imageio.stream.output.ImageOutputStreamAdapter;
import it.geosolutions.jaiext.colorindexer.ColorIndexer;
import it.geosolutions.jaiext.colorindexer.Quantizer;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.imageio.IIOImage;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import org.apache.log4j.Logger;
import org.geotools.image.ImageWorker;
import org.geotools.image.ImageWorker.PNGImageWriteParam;
import org.geowebcache.mime.ImageMime;
import org.geowebcache.mime.MimeType;
import com.sun.imageio.plugins.png.PNGImageWriter;
import com.sun.media.imageioimpl.plugins.clib.CLibImageWriter;
/**
* Class implementing the ImageEncoder interface, the user should only create a new bean for instantiating a new encoder object.
*/
public class ImageEncoderImpl implements ImageEncoder {
/**
* Logger used
*/
private static final Logger LOGGER = Logger.getLogger(ImageEncoderImpl.class);
/**
* Default string used for exceptions
*/
public static final String OPERATION_NOT_SUPPORTED = "Operation not supported";
/**Boolean indicating is aggressive outputstream is supported*/
private final boolean isAggressiveOutputStreamSupported;
/**Supported Mimetypes*/
private final List<String> supportedMimeTypes;
/**ImageReaderSpi object used*/
private ImageWriterSpi spi;
/** Map containing the input parameters used by the WriteHelper object*/
private Map<String, String> inputParams;
/**Helper object used for preparing Image and ImageWriteParam for writing the image*/
private WriteHelper helper;
/**
* This enum is used for preparing the image to write (prepareImage()) and the related ImageWriteParam(prepareParams()).
*/
public enum WriteHelper {
PNG("image/png", "image/png8", "image/png; mode=8bit", "image/png24",
"image/png; mode=24bit", "image/png;%20mode=24bit") {
public ImageWriteParam prepareParameters(ImageWriter writer, String compression,
boolean compressUsed, float compressionRate) {
ImageWriteParam params = null;
if (writer instanceof CLibImageWriter) {
params = writer.getDefaultWriteParam();
// Define compression mode
params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
if (compressUsed) {
// best compression
params.setCompressionType(compression);
}
if (compressionRate > -1) {
// we can control quality here
params.setCompressionQuality(compressionRate);
}
} else if (writer instanceof PNGImageWriter) {
params = new PNGImageWriteParam();
// Define compression mode
params.setCompressionMode(ImageWriteParam.MODE_DEFAULT);
}
return params;
}
public RenderedImage prepareImage(RenderedImage image, MimeType type) {
boolean isPNG8 = type == ImageMime.png8;
if (isPNG8) {
return applyPalette(image);
}
return image;
}
},
JPEG("image/jpeg") {
protected ImageWriteParam prepareParameters(ImageWriter writer, String compression,
boolean compressUsed, float compressionRate) {
ImageWriteParam params = writer.getDefaultWriteParam();
params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
if (compressUsed) {
// Lossy compression.
params.setCompressionType(compression);
}
if (compressionRate > -1) {
// we can control quality here
params.setCompressionQuality(compressionRate);
}
// If JPEGWriteParams, additional parameters are set
if (params instanceof JPEGImageWriteParam) {
final JPEGImageWriteParam jpegParams = (JPEGImageWriteParam) params;
jpegParams.setOptimizeHuffmanTables(true);
try {
jpegParams.setProgressiveMode(JPEGImageWriteParam.MODE_DEFAULT);
} catch (UnsupportedOperationException e) {
// Logged Exception
LOGGER.error(e.getMessage(), e);
}
params = jpegParams;
}
return params;
}
},
GIF("image/gif"){
public RenderedImage prepareImage(RenderedImage image, MimeType type) {
return applyPalette(image);
}
}, TIFF("image/tiff"), BMP("image/bmp");
private String[] formatNames;
WriteHelper(String... formatNames) {
this.formatNames = formatNames;
}
public ImageWriteParam prepareParams(Map<String, String> inputParams, ImageWriter writer) {
// Selection of the compression type
String compression = inputParams.get("COMPRESSION");
// Boolean indicating if compression is present
boolean compressUsed = compression != null && !compression.isEmpty()
&& !compression.equalsIgnoreCase("null");
// Selection of the compression rate
String compressionRateValue = inputParams.get("COMPRESSION_RATE");
// Initial value for the compression rate
float compressionRate = -1;
// Evaluation of the compression rate
if (compressionRateValue != null) {
try {
compressionRate = Float.parseFloat(compressionRateValue);
} catch (NumberFormatException e) {
// Do nothing and skip compression rate
}
}
// Creation of the ImageWriteParams
ImageWriteParam params = prepareParameters(writer, compression, compressUsed,
compressionRate);
return params;
}
protected ImageWriteParam prepareParameters(ImageWriter writer, String compression,
boolean compressUsed, float compressionRate) {
// Parameters creation
ImageWriteParam params = writer.getDefaultWriteParam();
params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
if (compressUsed) {
// best compression
params.setCompressionType(compression);
}
if (compressionRate > -1) {
// we can control quality here
params.setCompressionQuality(compressionRate);
}
return params;
}
public RenderedImage prepareImage(RenderedImage image, MimeType type) {
return image;
}
private boolean isFormatNameAccepted(String formatName) {
boolean accepted = false;
for (String format : formatNames) {
accepted = format.equalsIgnoreCase(formatName);
if (accepted) {
break;
}
}
return accepted;
}
public static WriteHelper getWriteHelperForName(String formatName) {
if (PNG.isFormatNameAccepted(formatName)) {
return PNG;
} else if (JPEG.isFormatNameAccepted(formatName)) {
return JPEG;
} else if (GIF.isFormatNameAccepted(formatName)) {
return GIF;
} else if (TIFF.isFormatNameAccepted(formatName)) {
return TIFF;
} else if (BMP.isFormatNameAccepted(formatName)) {
return BMP;
}
return null;
}
}
/**
* Encodes the selected image with the defined output object. The user can set the aggressive outputStream if supported.
*
* @param image Image to write.
* @param destination Destination object where the image is written.
* @param aggressiveOutputStreamOptimization Parameter used if aggressive outputStream optimization must be used.
* @throws IOException
*/
public void encode(RenderedImage image, Object destination,
boolean aggressiveOutputStreamOptimization, MimeType type, Map<String, ?> map) throws Exception{
if (!isAggressiveOutputStreamSupported() && aggressiveOutputStreamOptimization) {
throw new UnsupportedOperationException(OPERATION_NOT_SUPPORTED);
}
// Selection of the first priority writerSpi
ImageWriterSpi newSpi = getWriterSpi();
if (newSpi != null) {
// Creation of the associated Writer
ImageWriter writer = null;
ImageOutputStream stream = null;
try {
writer = newSpi.createWriterInstance();
// Check if the input object is an OutputStream
if (destination instanceof OutputStream) {
// Use of the ImageOutputStreamAdapter
if (isAggressiveOutputStreamSupported()) {
stream = new ImageOutputStreamAdapter((OutputStream) destination);
} else {
stream = new MemoryCacheImageOutputStream((OutputStream) destination);
}
// Preparation of the ImageWriteParams
ImageWriteParam params = null;
RenderedImage finalImage = image;
if (helper != null) {
params = helper.prepareParams(inputParams, writer);
finalImage = helper.prepareImage(image, type);
}
// Image writing
writer.setOutput(stream);
writer.write(null, new IIOImage(finalImage, null, null), params);
} else {
throw new IllegalArgumentException("Wrong output object");
}
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
throw e;
} finally {
// Writer disposal
if (writer != null) {
writer.dispose();
}
// Stream closure
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
LOGGER.error(e.getMessage(), e);
}
stream = null;
}
}
}
}
/**
* Returns the ImageSpiWriter associated to
*
* @return
*/
ImageWriterSpi getWriterSpi() {
return spi;
}
/**
* Returns all the supported MimeTypes
*
* @return supportedMimeTypes List of all the supported Mime Types
*/
public List<String> getSupportedMimeTypes() {
return supportedMimeTypes;
}
/**
* Indicates if optimization on OutputStream can be used
*
* @return isAggressiveOutputStreamSupported Boolean indicating if the selected encoder supports an aggressive output stream optimization
*/
public boolean isAggressiveOutputStreamSupported() {
return isAggressiveOutputStreamSupported;
}
/**
* Creates a new Instance of ImageEncoder supporting or not OutputStream optimization, with the defined MimeTypes and Spi classes.
*
* @param aggressiveOutputStreamOptimization
* @param supportedMimeTypes
* @param writerSpi
*/
public ImageEncoderImpl(boolean aggressiveOutputStreamOptimization,
List<String> supportedMimeTypes, List<String> writerSpi, Map<String, String> inputParams,ImageIOInitializer initializer) {
this.isAggressiveOutputStreamSupported = aggressiveOutputStreamOptimization;
this.supportedMimeTypes = new ArrayList<String>(supportedMimeTypes);
this.inputParams = inputParams;
// Get the IIORegistry if needed
IIORegistry theRegistry = initializer.getRegistry();
// Checks for each Spi class if it is present and then it is added to the list.
for (String spi : writerSpi) {
try {
Class<?> clazz = Class.forName(spi);
ImageWriterSpi writer = (ImageWriterSpi) theRegistry
.getServiceProviderByClass(clazz);
if (writer != null) {
this.spi = writer;
break;
}
} catch (ClassNotFoundException e) {
LOGGER.error(e.getMessage(), e);
}
}
// Selection of the helper object associated to the following format
helper = WriteHelper.getWriteHelperForName(supportedMimeTypes.get(0));
}
/**
* Returns the WriteHelper object used
*/
protected WriteHelper getHelper() {
return helper;
}
private static RenderedImage applyPalette(RenderedImage canvas) {
if (!(canvas.getColorModel() instanceof IndexColorModel)) {
// try to force a RGBA setup
ImageWorker imageWorker = new ImageWorker(canvas);
RenderedImage image = imageWorker.rescaleToBytes().forceComponentColorModel()
.getRenderedImage();
ColorIndexer indexer = new Quantizer(256).subsample().buildColorIndexer(image);
// if we have an indexer transform the image
if (indexer != null) {
image = new ImageWorker(image).colorIndex(indexer).getRenderedImage();
}
return image;
}
return canvas;
}
}